'XMPReader' => __DIR__ . '/includes/media/XMP.php',
'XMPValidate' => __DIR__ . '/includes/media/XMPValidate.php',
'Xhprof' => __DIR__ . '/includes/libs/Xhprof.php',
+ 'XhprofData' => __DIR__ . '/includes/libs/XhprofData.php',
'Xml' => __DIR__ . '/includes/Xml.php',
'XmlDumpWriter' => __DIR__ . '/includes/export/XmlDumpWriter.php',
'XmlJsCode' => __DIR__ . '/includes/Xml.php',
* @file
*/
-use RunningStat\RunningStat;
-
/**
* Convenience class for working with XHProf
* <https://github.com/phacility/xhprof>. XHProf can be installed as a PECL
* package for use with PHP5 (Zend PHP) and is built-in to HHVM 3.3.0.
*
- * @author Bryan Davis <bd808@wikimedia.org>
- * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
- * @since 1.25
+ * @since 1.28
*/
class Xhprof {
-
- /**
- * @var array $config
- */
- protected $config;
-
- /**
- * Hierarchical profiling data returned by xhprof.
- * @var array $hieraData
- */
- protected $hieraData;
-
/**
- * Per-function inclusive data.
- * @var array $inclusive
+ * @var bool $enabled Whether XHProf is currently running.
*/
- protected $inclusive;
+ protected static $enabled;
/**
- * Per-function inclusive and exclusive data.
- * @var array $complete
+ * Start xhprof profiler
*/
- protected $complete;
-
- /**
- * Configuration data can contain:
- * - flags: Optional flags to add additional information to the
- * profiling data collected.
- * (XHPROF_FLAGS_NO_BUILTINS, XHPROF_FLAGS_CPU,
- * XHPROF_FLAGS_MEMORY)
- * - exclude: Array of function names to exclude from profiling.
- * - include: Array of function names to include in profiling.
- * - sort: Key to sort per-function reports on.
- *
- * Note: When running under HHVM, xhprof will always behave as though the
- * XHPROF_FLAGS_NO_BUILTINS flag has been used unless the
- * Eval.JitEnableRenameFunction option is enabled for the HHVM process.
- *
- * @param array $config
- */
- public function __construct( array $config = [] ) {
- $this->config = array_merge(
- [
- 'flags' => 0,
- 'exclude' => [],
- 'include' => null,
- 'sort' => 'wt',
- ],
- $config
- );
-
- xhprof_enable( $this->config['flags'], [
- 'ignored_functions' => $this->config['exclude']
- ] );
+ public static function isEnabled() {
+ return self::$enabled;
}
/**
- * Stop collecting profiling data.
- *
- * Only the first invocation of this method will effect the internal
- * object state. Subsequent calls will return the data collected by the
- * initial call.
- *
- * @return array Collected profiling data (possibly cached)
+ * Start xhprof profiler
*/
- public function stop() {
- if ( $this->hieraData === null ) {
- $this->hieraData = $this->pruneData( xhprof_disable() );
+ public static function enable( $flags = 0, $options = [] ) {
+ if ( self::isEnabled() ) {
+ throw new Exception( 'Xhprof profiling is already enabled.' );
}
- return $this->hieraData;
- }
-
- /**
- * Load raw data from a prior run for analysis.
- * Stops any existing data collection and clears internal caches.
- *
- * Any 'include' filters configured for this Xhprof instance will be
- * enforced on the data as it is loaded. 'exclude' filters will however
- * not be enforced as they are an XHProf intrinsic behavior.
- *
- * @param array $data
- * @see getRawData()
- */
- public function loadRawData( array $data ) {
- $this->stop();
- $this->inclusive = null;
- $this->complete = null;
- $this->hieraData = $this->pruneData( $data );
- }
-
- /**
- * Get raw data collected by xhprof.
- *
- * If data collection has not been stopped yet this method will halt
- * collection to gather the profiling data.
- *
- * Each key in the returned array is an edge label for the call graph in
- * the form "caller==>callee". There is once special case edge labled
- * simply "main()" which represents the global scope entry point of the
- * application.
- *
- * XHProf will collect different data depending on the flags that are used:
- * - ct: Number of matching events seen.
- * - wt: Inclusive elapsed wall time for this event in microseconds.
- * - cpu: Inclusive elapsed cpu time for this event in microseconds.
- * (XHPROF_FLAGS_CPU)
- * - mu: Delta of memory usage from start to end of callee in bytes.
- * (XHPROF_FLAGS_MEMORY)
- * - pmu: Delta of peak memory usage from start to end of callee in
- * bytes. (XHPROF_FLAGS_MEMORY)
- * - alloc: Delta of amount memory requested from malloc() by the callee,
- * in bytes. (XHPROF_FLAGS_MALLOC)
- * - free: Delta of amount of memory passed to free() by the callee, in
- * bytes. (XHPROF_FLAGS_MALLOC)
- *
- * @return array
- * @see stop()
- * @see getInclusiveMetrics()
- * @see getCompleteMetrics()
- */
- public function getRawData() {
- return $this->stop();
+ self::$enabled = true;
+ xhprof_enable( $flags, $options );
}
/**
- * Convert an xhprof data key into an array of ['parent', 'child']
- * function names.
- *
- * The resulting array is left padded with nulls, so a key
- * with no parent (eg 'main()') will return [null, 'function'].
+ * Stop xhprof profiler
*
- * @return array
+ * @return array|null xhprof data from the run, or null if xhprof was not running.
*/
- public static function splitKey( $key ) {
- return array_pad( explode( '==>', $key, 2 ), -2, null );
- }
-
- /**
- * Remove data for functions that are not included in the 'include'
- * configuration array.
- *
- * @param array $data Raw xhprof data
- * @return array
- */
- protected function pruneData( $data ) {
- if ( !$this->config['include'] ) {
- return $data;
- }
-
- $want = array_fill_keys( $this->config['include'], true );
- $want['main()'] = true;
-
- $keep = [];
- foreach ( $data as $key => $stats ) {
- list( $parent, $child ) = self::splitKey( $key );
- if ( isset( $want[$parent] ) || isset( $want[$child] ) ) {
- $keep[$key] = $stats;
- }
+ public static function disable() {
+ if ( self::isEnabled() ) {
+ self::$enabled = false;
+ return xhprof_disable();
}
- return $keep;
- }
-
- /**
- * Get the inclusive metrics for each function call. Inclusive metrics
- * for given function include the metrics for all functions that were
- * called from that function during the measurement period.
- *
- * If data collection has not been stopped yet this method will halt
- * collection to gather the profiling data.
- *
- * See getRawData() for a description of the metric that are returned for
- * each funcition call. The values for the wt, cpu, mu and pmu metrics are
- * arrays with these values:
- * - total: Cumulative value
- * - min: Minimum value
- * - mean: Mean (average) value
- * - max: Maximum value
- * - variance: Variance (spread) of the values
- *
- * @return array
- * @see getRawData()
- * @see getCompleteMetrics()
- */
- public function getInclusiveMetrics() {
- if ( $this->inclusive === null ) {
- // Make sure we have data to work with
- $this->stop();
-
- $main = $this->hieraData['main()'];
- $hasCpu = isset( $main['cpu'] );
- $hasMu = isset( $main['mu'] );
- $hasAlloc = isset( $main['alloc'] );
-
- $this->inclusive = [];
- foreach ( $this->hieraData as $key => $stats ) {
- list( $parent, $child ) = self::splitKey( $key );
- if ( !isset( $this->inclusive[$child] ) ) {
- $this->inclusive[$child] = [
- 'ct' => 0,
- 'wt' => new RunningStat(),
- ];
- if ( $hasCpu ) {
- $this->inclusive[$child]['cpu'] = new RunningStat();
- }
- if ( $hasMu ) {
- $this->inclusive[$child]['mu'] = new RunningStat();
- $this->inclusive[$child]['pmu'] = new RunningStat();
- }
- if ( $hasAlloc ) {
- $this->inclusive[$child]['alloc'] = new RunningStat();
- $this->inclusive[$child]['free'] = new RunningStat();
- }
- }
-
- $this->inclusive[$child]['ct'] += $stats['ct'];
- foreach ( $stats as $stat => $value ) {
- if ( $stat === 'ct' ) {
- continue;
- }
-
- if ( !isset( $this->inclusive[$child][$stat] ) ) {
- // Ignore unknown stats
- continue;
- }
-
- for ( $i = 0; $i < $stats['ct']; $i++ ) {
- $this->inclusive[$child][$stat]->addObservation(
- $value / $stats['ct']
- );
- }
- }
- }
-
- // Convert RunningStat instances to static arrays and add
- // percentage stats.
- foreach ( $this->inclusive as $func => $stats ) {
- foreach ( $stats as $name => $value ) {
- if ( $value instanceof RunningStat ) {
- $total = $value->m1 * $value->n;
- $percent = ( isset( $main[$name] ) && $main[$name] )
- ? 100 * $total / $main[$name]
- : 0;
- $this->inclusive[$func][$name] = [
- 'total' => $total,
- 'min' => $value->min,
- 'mean' => $value->m1,
- 'max' => $value->max,
- 'variance' => $value->m2,
- 'percent' => $percent,
- ];
- }
- }
- }
-
- uasort( $this->inclusive, self::makeSortFunction(
- $this->config['sort'], 'total'
- ) );
- }
- return $this->inclusive;
- }
-
- /**
- * Get the inclusive and exclusive metrics for each function call.
- *
- * If data collection has not been stopped yet this method will halt
- * collection to gather the profiling data.
- *
- * In addition to the normal data contained in the inclusive metrics, the
- * metrics have an additional 'exclusive' measurement which is the total
- * minus the totals of all child function calls.
- *
- * @return array
- * @see getRawData()
- * @see getInclusiveMetrics()
- */
- public function getCompleteMetrics() {
- if ( $this->complete === null ) {
- // Start with inclusive data
- $this->complete = $this->getInclusiveMetrics();
-
- foreach ( $this->complete as $func => $stats ) {
- foreach ( $stats as $stat => $value ) {
- if ( $stat === 'ct' ) {
- continue;
- }
- // Initialize exclusive data with inclusive totals
- $this->complete[$func][$stat]['exclusive'] = $value['total'];
- }
- // Add sapce for call tree information to be filled in later
- $this->complete[$func]['calls'] = [];
- $this->complete[$func]['subcalls'] = [];
- }
-
- foreach ( $this->hieraData as $key => $stats ) {
- list( $parent, $child ) = self::splitKey( $key );
- if ( $parent !== null ) {
- // Track call tree information
- $this->complete[$child]['calls'][$parent] = $stats;
- $this->complete[$parent]['subcalls'][$child] = $stats;
- }
-
- if ( isset( $this->complete[$parent] ) ) {
- // Deduct child inclusive data from exclusive data
- foreach ( $stats as $stat => $value ) {
- if ( $stat === 'ct' ) {
- continue;
- }
-
- if ( !isset( $this->complete[$parent][$stat] ) ) {
- // Ignore unknown stats
- continue;
- }
-
- $this->complete[$parent][$stat]['exclusive'] -= $value;
- }
- }
- }
-
- uasort( $this->complete, self::makeSortFunction(
- $this->config['sort'], 'exclusive'
- ) );
- }
- return $this->complete;
- }
-
- /**
- * Get a list of all callers of a given function.
- *
- * @param string $function Function name
- * @return array
- * @see getEdges()
- */
- public function getCallers( $function ) {
- $edges = $this->getCompleteMetrics();
- if ( isset( $edges[$function]['calls'] ) ) {
- return array_keys( $edges[$function]['calls'] );
- } else {
- return [];
- }
- }
-
- /**
- * Get a list of all callees from a given function.
- *
- * @param string $function Function name
- * @return array
- * @see getEdges()
- */
- public function getCallees( $function ) {
- $edges = $this->getCompleteMetrics();
- if ( isset( $edges[$function]['subcalls'] ) ) {
- return array_keys( $edges[$function]['subcalls'] );
- } else {
- return [];
- }
- }
-
- /**
- * Find the critical path for the given metric.
- *
- * @param string $metric Metric to find critical path for
- * @return array
- */
- public function getCriticalPath( $metric = 'wt' ) {
- $this->stop();
- $func = 'main()';
- $path = [
- $func => $this->hieraData[$func],
- ];
- while ( $func ) {
- $callees = $this->getCallees( $func );
- $maxCallee = null;
- $maxCall = null;
- foreach ( $callees as $callee ) {
- $call = "{$func}==>{$callee}";
- if ( $maxCall === null ||
- $this->hieraData[$call][$metric] >
- $this->hieraData[$maxCall][$metric]
- ) {
- $maxCallee = $callee;
- $maxCall = $call;
- }
- }
- if ( $maxCall !== null ) {
- $path[$maxCall] = $this->hieraData[$maxCall];
- }
- $func = $maxCallee;
- }
- return $path;
- }
-
- /**
- * Make a closure to use as a sort function. The resulting function will
- * sort by descending numeric values (largest value first).
- *
- * @param string $key Data key to sort on
- * @param string $sub Sub key to sort array values on
- * @return Closure
- */
- public static function makeSortFunction( $key, $sub ) {
- return function ( $a, $b ) use ( $key, $sub ) {
- if ( isset( $a[$key] ) && isset( $b[$key] ) ) {
- // Descending sort: larger values will be first in result.
- // Assumes all values are numeric.
- // Values for 'main()' will not have sub keys
- $valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key];
- $valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key];
- return $valB - $valA;
- } else {
- // Sort datum with the key before those without
- return isset( $a[$key] ) ? -1 : 1;
- }
- };
}
}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use RunningStat\RunningStat;
+
+/**
+ * Convenience class for working with XHProf profiling data
+ * <https://github.com/phacility/xhprof>. XHProf can be installed as a PECL
+ * package for use with PHP5 (Zend PHP) and is built-in to HHVM 3.3.0.
+ *
+ * @author Bryan Davis <bd808@wikimedia.org>
+ * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
+ * @since 1.28
+ */
+class XhprofData {
+
+ /**
+ * @var array $config
+ */
+ protected $config;
+
+ /**
+ * Hierarchical profiling data returned by xhprof.
+ * @var array $hieraData
+ */
+ protected $hieraData;
+
+ /**
+ * Per-function inclusive data.
+ * @var array $inclusive
+ */
+ protected $inclusive;
+
+ /**
+ * Per-function inclusive and exclusive data.
+ * @var array $complete
+ */
+ protected $complete;
+
+ /**
+ * Configuration data can contain:
+ * - include: Array of function names to include in profiling.
+ * - sort: Key to sort per-function reports on.
+ *
+ * @param array $data Xhprof profiling data, as returned by xhprof_disable()
+ * @param array $config
+ */
+ public function __construct( array $data, array $config = [] ) {
+ $this->config = array_merge( [
+ 'include' => null,
+ 'sort' => 'wt',
+ ], $config );
+
+ $this->hieraData = $this->pruneData( $data );
+ }
+
+ /**
+ * Get raw data collected by xhprof.
+ *
+ * Each key in the returned array is an edge label for the call graph in
+ * the form "caller==>callee". There is once special case edge labled
+ * simply "main()" which represents the global scope entry point of the
+ * application.
+ *
+ * XHProf will collect different data depending on the flags that are used:
+ * - ct: Number of matching events seen.
+ * - wt: Inclusive elapsed wall time for this event in microseconds.
+ * - cpu: Inclusive elapsed cpu time for this event in microseconds.
+ * (XHPROF_FLAGS_CPU)
+ * - mu: Delta of memory usage from start to end of callee in bytes.
+ * (XHPROF_FLAGS_MEMORY)
+ * - pmu: Delta of peak memory usage from start to end of callee in
+ * bytes. (XHPROF_FLAGS_MEMORY)
+ * - alloc: Delta of amount memory requested from malloc() by the callee,
+ * in bytes. (XHPROF_FLAGS_MALLOC)
+ * - free: Delta of amount of memory passed to free() by the callee, in
+ * bytes. (XHPROF_FLAGS_MALLOC)
+ *
+ * @return array
+ * @see getInclusiveMetrics()
+ * @see getCompleteMetrics()
+ */
+ public function getRawData() {
+ return $this->hieraData;
+ }
+
+ /**
+ * Convert an xhprof data key into an array of ['parent', 'child']
+ * function names.
+ *
+ * The resulting array is left padded with nulls, so a key
+ * with no parent (eg 'main()') will return [null, 'function'].
+ *
+ * @return array
+ */
+ public static function splitKey( $key ) {
+ return array_pad( explode( '==>', $key, 2 ), -2, null );
+ }
+
+ /**
+ * Remove data for functions that are not included in the 'include'
+ * configuration array.
+ *
+ * @param array $data Raw xhprof data
+ * @return array
+ */
+ protected function pruneData( $data ) {
+ if ( !$this->config['include'] ) {
+ return $data;
+ }
+
+ $want = array_fill_keys( $this->config['include'], true );
+ $want['main()'] = true;
+
+ $keep = [];
+ foreach ( $data as $key => $stats ) {
+ list( $parent, $child ) = self::splitKey( $key );
+ if ( isset( $want[$parent] ) || isset( $want[$child] ) ) {
+ $keep[$key] = $stats;
+ }
+ }
+ return $keep;
+ }
+
+ /**
+ * Get the inclusive metrics for each function call. Inclusive metrics
+ * for given function include the metrics for all functions that were
+ * called from that function during the measurement period.
+ *
+ * See getRawData() for a description of the metric that are returned for
+ * each funcition call. The values for the wt, cpu, mu and pmu metrics are
+ * arrays with these values:
+ * - total: Cumulative value
+ * - min: Minimum value
+ * - mean: Mean (average) value
+ * - max: Maximum value
+ * - variance: Variance (spread) of the values
+ *
+ * @return array
+ * @see getRawData()
+ * @see getCompleteMetrics()
+ */
+ public function getInclusiveMetrics() {
+ if ( $this->inclusive === null ) {
+ $main = $this->hieraData['main()'];
+ $hasCpu = isset( $main['cpu'] );
+ $hasMu = isset( $main['mu'] );
+ $hasAlloc = isset( $main['alloc'] );
+
+ $this->inclusive = [];
+ foreach ( $this->hieraData as $key => $stats ) {
+ list( $parent, $child ) = self::splitKey( $key );
+ if ( !isset( $this->inclusive[$child] ) ) {
+ $this->inclusive[$child] = [
+ 'ct' => 0,
+ 'wt' => new RunningStat(),
+ ];
+ if ( $hasCpu ) {
+ $this->inclusive[$child]['cpu'] = new RunningStat();
+ }
+ if ( $hasMu ) {
+ $this->inclusive[$child]['mu'] = new RunningStat();
+ $this->inclusive[$child]['pmu'] = new RunningStat();
+ }
+ if ( $hasAlloc ) {
+ $this->inclusive[$child]['alloc'] = new RunningStat();
+ $this->inclusive[$child]['free'] = new RunningStat();
+ }
+ }
+
+ $this->inclusive[$child]['ct'] += $stats['ct'];
+ foreach ( $stats as $stat => $value ) {
+ if ( $stat === 'ct' ) {
+ continue;
+ }
+
+ if ( !isset( $this->inclusive[$child][$stat] ) ) {
+ // Ignore unknown stats
+ continue;
+ }
+
+ for ( $i = 0; $i < $stats['ct']; $i++ ) {
+ $this->inclusive[$child][$stat]->addObservation(
+ $value / $stats['ct']
+ );
+ }
+ }
+ }
+
+ // Convert RunningStat instances to static arrays and add
+ // percentage stats.
+ foreach ( $this->inclusive as $func => $stats ) {
+ foreach ( $stats as $name => $value ) {
+ if ( $value instanceof RunningStat ) {
+ $total = $value->m1 * $value->n;
+ $percent = ( isset( $main[$name] ) && $main[$name] )
+ ? 100 * $total / $main[$name]
+ : 0;
+ $this->inclusive[$func][$name] = [
+ 'total' => $total,
+ 'min' => $value->min,
+ 'mean' => $value->m1,
+ 'max' => $value->max,
+ 'variance' => $value->m2,
+ 'percent' => $percent,
+ ];
+ }
+ }
+ }
+
+ uasort( $this->inclusive, self::makeSortFunction(
+ $this->config['sort'], 'total'
+ ) );
+ }
+ return $this->inclusive;
+ }
+
+ /**
+ * Get the inclusive and exclusive metrics for each function call.
+ *
+ * In addition to the normal data contained in the inclusive metrics, the
+ * metrics have an additional 'exclusive' measurement which is the total
+ * minus the totals of all child function calls.
+ *
+ * @return array
+ * @see getRawData()
+ * @see getInclusiveMetrics()
+ */
+ public function getCompleteMetrics() {
+ if ( $this->complete === null ) {
+ // Start with inclusive data
+ $this->complete = $this->getInclusiveMetrics();
+
+ foreach ( $this->complete as $func => $stats ) {
+ foreach ( $stats as $stat => $value ) {
+ if ( $stat === 'ct' ) {
+ continue;
+ }
+ // Initialize exclusive data with inclusive totals
+ $this->complete[$func][$stat]['exclusive'] = $value['total'];
+ }
+ // Add sapce for call tree information to be filled in later
+ $this->complete[$func]['calls'] = [];
+ $this->complete[$func]['subcalls'] = [];
+ }
+
+ foreach ( $this->hieraData as $key => $stats ) {
+ list( $parent, $child ) = self::splitKey( $key );
+ if ( $parent !== null ) {
+ // Track call tree information
+ $this->complete[$child]['calls'][$parent] = $stats;
+ $this->complete[$parent]['subcalls'][$child] = $stats;
+ }
+
+ if ( isset( $this->complete[$parent] ) ) {
+ // Deduct child inclusive data from exclusive data
+ foreach ( $stats as $stat => $value ) {
+ if ( $stat === 'ct' ) {
+ continue;
+ }
+
+ if ( !isset( $this->complete[$parent][$stat] ) ) {
+ // Ignore unknown stats
+ continue;
+ }
+
+ $this->complete[$parent][$stat]['exclusive'] -= $value;
+ }
+ }
+ }
+
+ uasort( $this->complete, self::makeSortFunction(
+ $this->config['sort'], 'exclusive'
+ ) );
+ }
+ return $this->complete;
+ }
+
+ /**
+ * Get a list of all callers of a given function.
+ *
+ * @param string $function Function name
+ * @return array
+ * @see getEdges()
+ */
+ public function getCallers( $function ) {
+ $edges = $this->getCompleteMetrics();
+ if ( isset( $edges[$function]['calls'] ) ) {
+ return array_keys( $edges[$function]['calls'] );
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * Get a list of all callees from a given function.
+ *
+ * @param string $function Function name
+ * @return array
+ * @see getEdges()
+ */
+ public function getCallees( $function ) {
+ $edges = $this->getCompleteMetrics();
+ if ( isset( $edges[$function]['subcalls'] ) ) {
+ return array_keys( $edges[$function]['subcalls'] );
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * Find the critical path for the given metric.
+ *
+ * @param string $metric Metric to find critical path for
+ * @return array
+ */
+ public function getCriticalPath( $metric = 'wt' ) {
+ $func = 'main()';
+ $path = [
+ $func => $this->hieraData[$func],
+ ];
+ while ( $func ) {
+ $callees = $this->getCallees( $func );
+ $maxCallee = null;
+ $maxCall = null;
+ foreach ( $callees as $callee ) {
+ $call = "{$func}==>{$callee}";
+ if ( $maxCall === null ||
+ $this->hieraData[$call][$metric] >
+ $this->hieraData[$maxCall][$metric]
+ ) {
+ $maxCallee = $callee;
+ $maxCall = $call;
+ }
+ }
+ if ( $maxCall !== null ) {
+ $path[$maxCall] = $this->hieraData[$maxCall];
+ }
+ $func = $maxCallee;
+ }
+ return $path;
+ }
+
+ /**
+ * Make a closure to use as a sort function. The resulting function will
+ * sort by descending numeric values (largest value first).
+ *
+ * @param string $key Data key to sort on
+ * @param string $sub Sub key to sort array values on
+ * @return Closure
+ */
+ public static function makeSortFunction( $key, $sub ) {
+ return function ( $a, $b ) use ( $key, $sub ) {
+ if ( isset( $a[$key] ) && isset( $b[$key] ) ) {
+ // Descending sort: larger values will be first in result.
+ // Assumes all values are numeric.
+ // Values for 'main()' will not have sub keys
+ $valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key];
+ $valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key];
+ return $valB - $valA;
+ } else {
+ // Sort datum with the key before those without
+ return isset( $a[$key] ) ? -1 : 1;
+ }
+ };
+ }
+}
*/
class ProfilerXhprof extends Profiler {
/**
- * @var Xhprof $xhprof
+ * @var XhprofData|null $xhprofData
*/
- protected $xhprof;
+ protected $xhprofData;
/**
* Profiler for explicit, arbitrary, frame labels
*/
public function __construct( array $params = [] ) {
parent::__construct( $params );
- $this->xhprof = new Xhprof( $params );
+
+ $flags = isset( $params['flags'] ) ? $params['flags'] : 0;
+ $options = isset( $params['exclude'] )
+ ? [ 'ignored_functions' => $params['exclude'] ] : [];
+ Xhprof::enable( $flags, $options );
$this->sprofiler = new SectionProfiler();
}
+ /**
+ * @return XhprofData
+ */
+ public function getXhprofData() {
+ if ( !$this->xhprofData ) {
+ $this->xhprofData = new XhprofData( Xhprof::disable(), $this->params );
+ }
+ return $this->xhprofData;
+ }
+
public function scopedProfileIn( $section ) {
$key = 'section.' . ltrim( $section, '.' );
return $this->sprofiler->scopedProfileIn( $key );
}
public function getFunctionStats() {
- $metrics = $this->xhprof->getCompleteMetrics();
+ $metrics = $this->getXhprofData()->getCompleteMetrics();
$profile = [];
$main = null; // units in ms
* @return array
*/
public function getRawData() {
- return $this->xhprof->getRawData();
+ return $this->getXhprofData()->getRawData();
}
}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @uses XhprofData
+ * @uses AutoLoader
+ * @author Bryan Davis <bd808@wikimedia.org>
+ * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
+ * @since 1.25
+ */
+class XhprofDataTest extends PHPUnit_Framework_TestCase {
+
+ /**
+ * @covers XhprofData::splitKey
+ * @dataProvider provideSplitKey
+ */
+ public function testSplitKey( $key, $expect ) {
+ $this->assertSame( $expect, XhprofData::splitKey( $key ) );
+ }
+
+ public function provideSplitKey() {
+ return [
+ [ 'main()', [ null, 'main()' ] ],
+ [ 'foo==>bar', [ 'foo', 'bar' ] ],
+ [ 'bar@1==>bar@2', [ 'bar@1', 'bar@2' ] ],
+ [ 'foo==>bar==>baz', [ 'foo', 'bar==>baz' ] ],
+ [ '==>bar', [ '', 'bar' ] ],
+ [ '', [ null, '' ] ],
+ ];
+ }
+
+ /**
+ * @covers XhprofData::pruneData
+ */
+ public function testInclude() {
+ $xhprofData = $this->getXhprofDataFixture( [
+ 'include' => [ 'main()' ],
+ ] );
+ $raw = $xhprofData->getRawData();
+ $this->assertArrayHasKey( 'main()', $raw );
+ $this->assertArrayHasKey( 'main()==>foo', $raw );
+ $this->assertArrayHasKey( 'main()==>xhprof_disable', $raw );
+ $this->assertSame( 3, count( $raw ) );
+ }
+
+ /**
+ * Validate the structure of data returned by
+ * Xhprof::getInclusiveMetrics(). This acts as a guard against unexpected
+ * structural changes to the returned data in lieu of using a more heavy
+ * weight typed response object.
+ *
+ * @covers XhprofData::getInclusiveMetrics
+ */
+ public function testInclusiveMetricsStructure() {
+ $metricStruct = [
+ 'ct' => 'int',
+ 'wt' => 'array',
+ 'cpu' => 'array',
+ 'mu' => 'array',
+ 'pmu' => 'array',
+ ];
+ $statStruct = [
+ 'total' => 'numeric',
+ 'min' => 'numeric',
+ 'mean' => 'numeric',
+ 'max' => 'numeric',
+ 'variance' => 'numeric',
+ 'percent' => 'numeric',
+ ];
+
+ $xhprofData = $this->getXhprofDataFixture();
+ $metrics = $xhprofData->getInclusiveMetrics();
+
+ foreach ( $metrics as $name => $metric ) {
+ $this->assertArrayStructure( $metricStruct, $metric );
+
+ foreach ( $metricStruct as $key => $type ) {
+ if ( $type === 'array' ) {
+ $this->assertArrayStructure( $statStruct, $metric[$key] );
+ if ( $name === 'main()' ) {
+ $this->assertEquals( 100, $metric[$key]['percent'] );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Validate the structure of data returned by
+ * Xhprof::getCompleteMetrics(). This acts as a guard against unexpected
+ * structural changes to the returned data in lieu of using a more heavy
+ * weight typed response object.
+ *
+ * @covers XhprofData::getCompleteMetrics
+ */
+ public function testCompleteMetricsStructure() {
+ $metricStruct = [
+ 'ct' => 'int',
+ 'wt' => 'array',
+ 'cpu' => 'array',
+ 'mu' => 'array',
+ 'pmu' => 'array',
+ 'calls' => 'array',
+ 'subcalls' => 'array',
+ ];
+ $statsMetrics = [ 'wt', 'cpu', 'mu', 'pmu' ];
+ $statStruct = [
+ 'total' => 'numeric',
+ 'min' => 'numeric',
+ 'mean' => 'numeric',
+ 'max' => 'numeric',
+ 'variance' => 'numeric',
+ 'percent' => 'numeric',
+ 'exclusive' => 'numeric',
+ ];
+
+ $xhprofData = $this->getXhprofDataFixture();
+ $metrics = $xhprofData->getCompleteMetrics();
+
+ foreach ( $metrics as $name => $metric ) {
+ $this->assertArrayStructure( $metricStruct, $metric, $name );
+
+ foreach ( $metricStruct as $key => $type ) {
+ if ( in_array( $key, $statsMetrics ) ) {
+ $this->assertArrayStructure(
+ $statStruct, $metric[$key], $key
+ );
+ $this->assertLessThanOrEqual(
+ $metric[$key]['total'], $metric[$key]['exclusive']
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * @covers XhprofData::getCallers
+ * @covers XhprofData::getCallees
+ * @uses XhprofData
+ */
+ public function testEdges() {
+ $xhprofData = $this->getXhprofDataFixture();
+ $this->assertSame( [], $xhprofData->getCallers( 'main()' ) );
+ $this->assertSame( [ 'foo', 'xhprof_disable' ],
+ $xhprofData->getCallees( 'main()' )
+ );
+ $this->assertSame( [ 'main()' ],
+ $xhprofData->getCallers( 'foo' )
+ );
+ $this->assertSame( [], $xhprofData->getCallees( 'strlen' ) );
+ }
+
+ /**
+ * @covers XhprofData::getCriticalPath
+ * @uses XhprofData
+ */
+ public function testCriticalPath() {
+ $xhprofData = $this->getXhprofDataFixture();
+ $path = $xhprofData->getCriticalPath();
+
+ $last = null;
+ foreach ( $path as $key => $value ) {
+ list( $func, $call ) = XhprofData::splitKey( $key );
+ $this->assertSame( $last, $func );
+ $last = $call;
+ }
+ $this->assertSame( $last, 'bar@1' );
+ }
+
+ /**
+ * Get an Xhprof instance that has been primed with a set of known testing
+ * data. Tests for the Xhprof class should laregly be concerned with
+ * evaluating the manipulations of the data collected by xhprof rather
+ * than the data collection process itself.
+ *
+ * The returned Xhprof instance primed will be with a data set created by
+ * running this trivial program using the PECL xhprof implementation:
+ * @code
+ * function bar( $x ) {
+ * if ( $x > 0 ) {
+ * bar($x - 1);
+ * }
+ * }
+ * function foo() {
+ * for ( $idx = 0; $idx < 2; $idx++ ) {
+ * bar( $idx );
+ * $x = strlen( 'abc' );
+ * }
+ * }
+ * xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY );
+ * foo();
+ * $x = xhprof_disable();
+ * var_export( $x );
+ * @endcode
+ *
+ * @return Xhprof
+ */
+ protected function getXhprofDataFixture( array $opts = [] ) {
+ return new XhprofData( [
+ 'foo==>bar' => [
+ 'ct' => 2,
+ 'wt' => 57,
+ 'cpu' => 92,
+ 'mu' => 1896,
+ 'pmu' => 0,
+ ],
+ 'foo==>strlen' => [
+ 'ct' => 2,
+ 'wt' => 21,
+ 'cpu' => 141,
+ 'mu' => 752,
+ 'pmu' => 0,
+ ],
+ 'bar==>bar@1' => [
+ 'ct' => 1,
+ 'wt' => 18,
+ 'cpu' => 19,
+ 'mu' => 752,
+ 'pmu' => 0,
+ ],
+ 'main()==>foo' => [
+ 'ct' => 1,
+ 'wt' => 304,
+ 'cpu' => 307,
+ 'mu' => 4008,
+ 'pmu' => 0,
+ ],
+ 'main()==>xhprof_disable' => [
+ 'ct' => 1,
+ 'wt' => 8,
+ 'cpu' => 10,
+ 'mu' => 768,
+ 'pmu' => 392,
+ ],
+ 'main()' => [
+ 'ct' => 1,
+ 'wt' => 353,
+ 'cpu' => 351,
+ 'mu' => 6112,
+ 'pmu' => 1424,
+ ],
+ ], $opts );
+ }
+
+ /**
+ * Assert that the given array has the described structure.
+ *
+ * @param array $struct Array of key => type mappings
+ * @param array $actual Array to check
+ * @param string $label
+ */
+ protected function assertArrayStructure( $struct, $actual, $label = null ) {
+ $this->assertInternalType( 'array', $actual, $label );
+ $this->assertCount( count( $struct ), $actual, $label );
+ foreach ( $struct as $key => $type ) {
+ $this->assertArrayHasKey( $key, $actual );
+ $this->assertInternalType( $type, $actual[$key] );
+ }
+ }
+}
* @file
*/
-/**
- * @uses Xhprof
- * @uses AutoLoader
- * @author Bryan Davis <bd808@wikimedia.org>
- * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
- * @since 1.25
- */
class XhprofTest extends PHPUnit_Framework_TestCase {
-
- public function setUp() {
- if ( !function_exists( 'xhprof_enable' ) ) {
- $this->markTestSkipped( 'No xhprof support detected.' );
- }
- }
-
- /**
- * @covers Xhprof::splitKey
- * @dataProvider provideSplitKey
- */
- public function testSplitKey( $key, $expect ) {
- $this->assertSame( $expect, Xhprof::splitKey( $key ) );
- }
-
- public function provideSplitKey() {
- return [
- [ 'main()', [ null, 'main()' ] ],
- [ 'foo==>bar', [ 'foo', 'bar' ] ],
- [ 'bar@1==>bar@2', [ 'bar@1', 'bar@2' ] ],
- [ 'foo==>bar==>baz', [ 'foo', 'bar==>baz' ] ],
- [ '==>bar', [ '', 'bar' ] ],
- [ '', [ null, '' ] ],
- ];
- }
-
- /**
- * @covers Xhprof::__construct
- * @covers Xhprof::stop
- * @covers Xhprof::getRawData
- * @dataProvider provideRawData
- */
- public function testRawData( $flags, $keys ) {
- $xhprof = new Xhprof( [ 'flags' => $flags ] );
- $raw = $xhprof->getRawData();
- $this->assertArrayHasKey( 'main()', $raw );
- foreach ( $keys as $key ) {
- $this->assertArrayHasKey( $key, $raw['main()'] );
- }
- }
-
- public function provideRawData() {
- $tests = [
- [ 0, [ 'ct', 'wt' ] ],
- ];
-
- if ( defined( 'XHPROF_FLAGS_CPU' ) && defined( 'XHPROF_FLAGS_CPU' ) ) {
- $tests[] = [ XHPROF_FLAGS_MEMORY, [
- 'ct', 'wt', 'mu', 'pmu',
- ] ];
- $tests[] = [ XHPROF_FLAGS_CPU, [
- 'ct', 'wt', 'cpu',
- ] ];
- $tests[] = [ XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_CPU, [
- 'ct', 'wt', 'mu', 'pmu', 'cpu',
- ] ];
- }
-
- return $tests;
- }
-
- /**
- * @covers Xhprof::pruneData
- */
- public function testInclude() {
- $xhprof = $this->getXhprofFixture( [
- 'include' => [ 'main()' ],
- ] );
- $raw = $xhprof->getRawData();
- $this->assertArrayHasKey( 'main()', $raw );
- $this->assertArrayHasKey( 'main()==>foo', $raw );
- $this->assertArrayHasKey( 'main()==>xhprof_disable', $raw );
- $this->assertSame( 3, count( $raw ) );
- }
-
/**
- * Validate the structure of data returned by
- * Xhprof::getInclusiveMetrics(). This acts as a guard against unexpected
- * structural changes to the returned data in lieu of using a more heavy
- * weight typed response object.
+ * Trying to enable Xhprof when it is already enabled causes an exception
+ * to be thrown.
*
- * @covers Xhprof::getInclusiveMetrics
- */
- public function testInclusiveMetricsStructure() {
- $metricStruct = [
- 'ct' => 'int',
- 'wt' => 'array',
- 'cpu' => 'array',
- 'mu' => 'array',
- 'pmu' => 'array',
- ];
- $statStruct = [
- 'total' => 'numeric',
- 'min' => 'numeric',
- 'mean' => 'numeric',
- 'max' => 'numeric',
- 'variance' => 'numeric',
- 'percent' => 'numeric',
- ];
-
- $xhprof = $this->getXhprofFixture();
- $metrics = $xhprof->getInclusiveMetrics();
-
- foreach ( $metrics as $name => $metric ) {
- $this->assertArrayStructure( $metricStruct, $metric );
-
- foreach ( $metricStruct as $key => $type ) {
- if ( $type === 'array' ) {
- $this->assertArrayStructure( $statStruct, $metric[$key] );
- if ( $name === 'main()' ) {
- $this->assertEquals( 100, $metric[$key]['percent'] );
- }
- }
- }
- }
- }
-
- /**
- * Validate the structure of data returned by
- * Xhprof::getCompleteMetrics(). This acts as a guard against unexpected
- * structural changes to the returned data in lieu of using a more heavy
- * weight typed response object.
- *
- * @covers Xhprof::getCompleteMetrics
- */
- public function testCompleteMetricsStructure() {
- $metricStruct = [
- 'ct' => 'int',
- 'wt' => 'array',
- 'cpu' => 'array',
- 'mu' => 'array',
- 'pmu' => 'array',
- 'calls' => 'array',
- 'subcalls' => 'array',
- ];
- $statsMetrics = [ 'wt', 'cpu', 'mu', 'pmu' ];
- $statStruct = [
- 'total' => 'numeric',
- 'min' => 'numeric',
- 'mean' => 'numeric',
- 'max' => 'numeric',
- 'variance' => 'numeric',
- 'percent' => 'numeric',
- 'exclusive' => 'numeric',
- ];
-
- $xhprof = $this->getXhprofFixture();
- $metrics = $xhprof->getCompleteMetrics();
-
- foreach ( $metrics as $name => $metric ) {
- $this->assertArrayStructure( $metricStruct, $metric, $name );
-
- foreach ( $metricStruct as $key => $type ) {
- if ( in_array( $key, $statsMetrics ) ) {
- $this->assertArrayStructure(
- $statStruct, $metric[$key], $key
- );
- $this->assertLessThanOrEqual(
- $metric[$key]['total'], $metric[$key]['exclusive']
- );
- }
- }
- }
- }
-
- /**
- * @covers Xhprof::getCallers
- * @covers Xhprof::getCallees
- * @uses Xhprof
- */
- public function testEdges() {
- $xhprof = $this->getXhprofFixture();
- $this->assertSame( [], $xhprof->getCallers( 'main()' ) );
- $this->assertSame( [ 'foo', 'xhprof_disable' ],
- $xhprof->getCallees( 'main()' )
- );
- $this->assertSame( [ 'main()' ],
- $xhprof->getCallers( 'foo' )
- );
- $this->assertSame( [], $xhprof->getCallees( 'strlen' ) );
- }
-
- /**
- * @covers Xhprof::getCriticalPath
- * @uses Xhprof
- */
- public function testCriticalPath() {
- $xhprof = $this->getXhprofFixture();
- $path = $xhprof->getCriticalPath();
-
- $last = null;
- foreach ( $path as $key => $value ) {
- list( $func, $call ) = Xhprof::splitKey( $key );
- $this->assertSame( $last, $func );
- $last = $call;
- }
- $this->assertSame( $last, 'bar@1' );
- }
-
- /**
- * Get an Xhprof instance that has been primed with a set of known testing
- * data. Tests for the Xhprof class should laregly be concerned with
- * evaluating the manipulations of the data collected by xhprof rather
- * than the data collection process itself.
- *
- * The returned Xhprof instance primed will be with a data set created by
- * running this trivial program using the PECL xhprof implementation:
- * @code
- * function bar( $x ) {
- * if ( $x > 0 ) {
- * bar($x - 1);
- * }
- * }
- * function foo() {
- * for ( $idx = 0; $idx < 2; $idx++ ) {
- * bar( $idx );
- * $x = strlen( 'abc' );
- * }
- * }
- * xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY );
- * foo();
- * $x = xhprof_disable();
- * var_export( $x );
- * @endcode
- *
- * @return Xhprof
- */
- protected function getXhprofFixture( array $opts = [] ) {
- $xhprof = new Xhprof( $opts );
- $xhprof->loadRawData( [
- 'foo==>bar' => [
- 'ct' => 2,
- 'wt' => 57,
- 'cpu' => 92,
- 'mu' => 1896,
- 'pmu' => 0,
- ],
- 'foo==>strlen' => [
- 'ct' => 2,
- 'wt' => 21,
- 'cpu' => 141,
- 'mu' => 752,
- 'pmu' => 0,
- ],
- 'bar==>bar@1' => [
- 'ct' => 1,
- 'wt' => 18,
- 'cpu' => 19,
- 'mu' => 752,
- 'pmu' => 0,
- ],
- 'main()==>foo' => [
- 'ct' => 1,
- 'wt' => 304,
- 'cpu' => 307,
- 'mu' => 4008,
- 'pmu' => 0,
- ],
- 'main()==>xhprof_disable' => [
- 'ct' => 1,
- 'wt' => 8,
- 'cpu' => 10,
- 'mu' => 768,
- 'pmu' => 392,
- ],
- 'main()' => [
- 'ct' => 1,
- 'wt' => 353,
- 'cpu' => 351,
- 'mu' => 6112,
- 'pmu' => 1424,
- ],
- ] );
- return $xhprof;
- }
-
- /**
- * Assert that the given array has the described structure.
- *
- * @param array $struct Array of key => type mappings
- * @param array $actual Array to check
- * @param string $label
- */
- protected function assertArrayStructure( $struct, $actual, $label = null ) {
- $this->assertInternalType( 'array', $actual, $label );
- $this->assertCount( count( $struct ), $actual, $label );
- foreach ( $struct as $key => $type ) {
- $this->assertArrayHasKey( $key, $actual );
- $this->assertInternalType( $type, $actual[$key] );
- }
+ * @expectedException Exception
+ * @expectedExceptionMessage already enabled
+ * @covers Xhprof::enable
+ */
+ public function testEnable() {
+ $xhprof = new ReflectionClass( 'Xhprof' );
+ $enabled = $xhprof->getProperty( 'enabled' );
+ $enabled->setAccessible( true );
+ $enabled->setValue( true );
+ $xhprof->getMethod( 'enable' )->invoke( null );
}
}